05. Prototypal Inheritance

OOJS L3 59 - Prototypal Inheritence V2

Adding Methods to the Prototype

Recall that objects contain data (i.e., properties), as well as the means to manipulate that data (i.e., methods). Earlier in this Lesson, we simply added methods directly into the constructor function itself:

function Cat(name) {
 this.lives = 9;
 this.name = name;

 this.sayName = function () {
   console.log(`Meow! My name is ${this.name}`);
 };
}

This way, a sayName method gets added to all Cat objects by saving a function to the sayName attribute of newly-created Cat objects.

This works just fine, but what if we want to instantiate more and more Cat objects with this constructor? You'll create a new function every single time for that Cat object's sayName! What's more: if you ever want to make changes to the method, you'll have to update all objects individually. In this situation, it makes sense to have all objects created by the same Cat constructor function just share a single sayName method.

To save memory and keep things DRY, we can add methods to the constructor function's prototype property. The prototype is just an object, and all objects created by a constructor function keep a reference to the prototype. Those objects can even use the prototype's properties as their own!

JavaScript leverages this secret link -- between an object and its prototype -- to implement inheritance. Consider the following prototype chain:

_The `Cat()` constructor function is invoked using the `new` operator, which creates the `bailey` instance (object). Note that the `meow()` method is defined in the prototype of the `bailey` object's constructor function. The prototype is just an object, and all objects created by that constructor are secretly linked to the prototype. As such, we can execute `bailey.meow()` as if it were `bailey`'s own method!_

The Cat() constructor function is invoked using the new operator, which creates the bailey instance (object). Note that the meow() method is defined in the prototype of the bailey object's constructor function. The prototype is just an object, and all objects created by that constructor are secretly linked to the prototype. As such, we can execute bailey.meow() as if it were bailey's own method!

Recall that each function has a prototype property, which is really just an object. When this function is invoked as a constructor using the new operator, it creates and returns a new object. This object is secretly linked to its constructor's prototype, and this secret link allows the object to access the prototype's properties and methods as if it were its own!

Since we know that the prototype property just points to a regular object, that object itself also has a secret link to its prototype. And that prototype object also has reference to its own prototype -- and so on. This is how the prototype chain is formed.

Finding Properties and Methods on the Prototype Chain

Whether you're accessing a property (e.g., bailey.lives;) or invoking a method (e.g., bailey.meow();), the JavaScript interpreter looks for them along the prototype chain in a very particular order:

  1. First, the JavaScript engine will look at the object's own properties. This means that any properties and methods defined directly in the object itself will take precedence over any properties and methods elsewhere if their names are the same (similar to variable shadowing in the scope chain).
  2. If it doesn't find the property in question, it will then search the object's constructor's prototype for a match.
  3. If the property doesn't exist in the prototype, the JavaScript engine will continue looking up the chain.
  4. At the very end of the chain is the Object() object, or the top-level parent. If the property still cannot be found, the property is undefined.

Previously, we simply defined methods directly in a constructor function itself. Let's see how things look if we defined methods in the constructor's prototype instead!

L3- 62 - Methods And Prototype FIXED

For the next quiz, consider the following two code snippets below (i.e., A and B):

// (A)

function Dalmatian (name) {
  this.name = name;

  this.bark = function() {
    console.log(`${this.name} barks!`);
  };
}
// (B)

function Dalmatian (name) {
  this.name = name;
}

Dalmatian.prototype.bark = function() {
  console.log(`${this.name} barks!`);
};

Let's say that we want to define a method that can be invoked on instances (objects) of the Dalmatian constructor function (we'll be instantiating at least 101 of them!).

Which of the preceding two approaches is optimal?

SOLUTION: **(B)** is optimal, because the function that `bark` points to does not need to be recreated each time an instance of `Dalmatian` is created.

💡 Replacing the prototype Object 💡

What happens if you completely replace a function's prototype object? How does this affect objects created by that function? Let's look at a simple Hamster constructor function and instantiate a few objects:

function Hamster() {
  this.hasFur = true;
}

let waffle = new Hamster();
let pancake = new Hamster();

First, note that even after we make the new objects, waffle and pancake, we can still add properties to Hamster's prototype and it will still be able to access those new properties.

Hamster.prototype.eat = function () {
  console.log('Chomp chomp chomp!');
};

waffle.eat();
// 'Chomp chomp chomp!'

pancake.eat();
// 'Chomp chomp chomp!'

Now, let's replace Hamster's prototype object with something else entirely:

Hamster.prototype = {
  isHungry: false,
  color: 'brown'
};

The previous objects don't have access to the updated prototype's properties; they just retain their secret link to the old prototype:

console.log(waffle.color);
// undefined

waffle.eat();
// 'Chomp chomp chomp!'

console.log(pancake.isHungry);
// undefined

As it turns out, any new Hamster objects created moving forward will use the updated prototype:

const muffin = new Hamster();

muffin.eat();
// TypeError: muffin.eat is not a function

console.log(muffin.isHungry);
// false

console.log(muffin.color);
// 'brown'

L3 - 70 - Prototype Demo

Checking an Object's Properties

As we've just seen, if an object doesn't have a particular property of its own, it can access one somewhere along the prototype chain (assuming it exists, of course). With so many options, it can sometimes get tricky to tell just where a particular property is coming from! Here are a few useful methods to help you along the way.

hasOwnProperty()

hasOwnProperty() allows you to find the origin of a particular property. Upon passing in a string of the property name you're looking for, the method will return a boolean indicating whether or not the property belongs to the object itself (i.e., that property was not inherited). Consider the Phone constructor with a single property defined directly in the function, and another property on its prototype object:

function Phone() {
  this.operatingSystem = 'Android';
}

Phone.prototype.screenSize = 6;

Let's now create a new object, myPhone, and check whether operatingSystem is its own property, meaning that it was not inherited from its prototype (or somewhere else along the prototype chain):

const myPhone = new Phone();

const own = myPhone.hasOwnProperty('operatingSystem');

console.log(own);
// true

Indeed it returns true! What about the screenSize property, which exists on Phone objects' prototype?

const inherited = myPhone.hasOwnProperty('screenSize');

console.log(inherited);
// false

Using hasOwnProperty(), we gain insight a certain property's origins.

isPrototypeOf()

Objects also have access to the isPrototypeOf() method, which checks whether or not an object exists in another object's prototype chain. Using this method, you can confirm if a particular object serves as the prototype of another object. Check out the following rodent object:

const rodent = {
  favoriteFood: 'cheese',
  hasTail: true
};

Let's now build a Mouse() constructor function, and assign its prototype to rodent:

function Mouse() {
  this.favoriteFood = 'cheese';
}

Mouse.prototype = rodent;

If we create a new Mouse object, its prototype should be the rodent object. Let's confirm:

const ralph = new Mouse();

const result = rodent.isPrototypeOf(ralph);

console.log(result);
// true

Great! isPrototypeOf() is a great way to confirm if an object exists in another object's prototype chain.

Object.getPrototypeOf()

isPrototypeOf() works well, but keep in mind that in order to use it, you must have that prototype object at hand in the first place! What if you're not sure what a certain object's prototype is? Object.getPrototypeOf() can help with just that!

Using the previous example, let's store the return value of Object.getPrototypeOf() in a variable, myPrototype, then check what it is:

const myPrototype = Object.getPrototypeOf(ralph);

console.log(myPrototype);
// { favoriteFood: 'cheese', hasTail: true }

Great! The prototype of ralph has the same properties as the result because they are the same object. Object.getPrototypeOf() is great for retrieving the prototype of a given object.

The constructor Property

Each time an object is created, a special property is assigned to it under the hood: constructor. Accessing an object's constructor property returns a reference to the constructor function that created that object in the first place! Here's a simple Longboard constructor function. We'll also go ahead and make a new object, then save it to a board variable:

function Longboard() {
  this.material = 'bamboo';
}

const board = new Longboard();

If we access board's constructor property, we should see the original constructor function itself:

console.log(board.constructor);

// function Longboard() {
//   this.material = 'bamboo';
// }

Excellent! Keep in mind that if an object was created using literal notation, its constructor is the built-in Object() constructor function!

const rodent = {
  favoriteFood: 'cheese',
  hasTail: true
};

console.log(rodent.constructor);
// function Object() { [native code] }

What is true about hasOwnProperty()? Select all that apply:

SOLUTION:
  • It returns a boolean indicating whether the object has the specified property as its own property (i.e., the property isn't inherited)
  • `hasOwnProperty()` is invoked as a method onto an object

What is true about isPrototypeOf() or getPrototypeOf()? Select all that apply:

SOLUTION:
  • `isPrototypeOf()` checks whether or not an object exists in another object's prototype chain
  • `isPrototypeOf()` takes a single argument: an object whose prototype chain is to be searched
  • `getPrototypeOf()` returns the prototype of the object passed into it

What is true about constructor property? Select all that apply:

SOLUTION:
  • Accessing an object's `constructor` property returns a reference to the constructor function that created that object (instance)
  • Every object has a `constructor` property
  • Objects created with literal notation are constructed with the `Object()` constructor function

Let's say that we create the following object, capitals, using regular object literal notation:

const capitals = {
  California: 'Sacramento',
  Washington: 'Olympia',
  Oregon: 'Salem',
  Texas: 'Austin'
};

What is returned when Object.getPrototypeOf(capitals); is executed?

SOLUTION: A reference to `Object()`'s prototype

Summary

Inheritance in JavaScript is when an object is based on another object. Inheritance allows us to reuse existing code, having objects take on properties of other objects.

When a function is called as a constructor using the new operator, the function creates and returns a new object. This object is secretly linked to its constructor's prototype, which is just another object. Using this secret link allows an object to access the prototype's properties and methods as if it were its own. If JavaScript does not find a particular property within an object, it will keep looking up the prototype chain, eventually reaching Object() (top-level parent) if necessary.

We also looked at a few methods and properties that allow use to check the origins and references of objects and their prototypes, namely:

  • hasOwnProperty()
  • isPrototypeOf()
  • Object.getPrototypeOf()
  • .constructor

In the next section, we'll check out another part of prototypal inheritance in the form of subclassing. What if you want to inherit just a few properties from an object -- but want an object to also have other, specialized properties of their own? We'll take an even deeper dive into prototypal inheritance in the next section!